type Border = { xMin: number; yMin: number; xMax: number; yMax: number; }
type RSize = { r: number; d: number; } // 半径，直径
type Position = { x: number; y: number; angle: number; }
type Speed = { x: number; y: number; angle: number; }

const FLAG_OVER:number = 99999999;
class Stuff {
  constructor(
    public size: RSize,
    public pos: Position, 
    public speed: Speed,
    public game: Game,
  ) { }

  clear(context:any) {
    context.clearRect(
      this.pos.x - (Math.floor(this.size.r) + 1),
      this.pos.y - (Math.floor(this.size.r) + 1),
      this.size.d + 2,
      this.size.d + 2,
    );
  }

  move(offsetX?: number) {
    this.pos.angle += this.speed.angle;
    this.pos.x += this.speed.x;
    this.pos.y += this.speed.y;
    this.pos.x += offsetX ?? this.speed.x;
    if (this.pos.x < 0) {
      this.pos.x = 0;
      if (this.speed.x !== 0) {
        this.speed.x = -this.speed.x;
      }
    }
    if (this.pos.x > this.game.border.xMax) {
      this.pos.x = this.game.border.xMax;
      if (this.speed.x !== 0) {
        this.speed.x = -this.speed.x;
      }
    }
    if (this.pos.y < 0) {
      this.pos.y = FLAG_OVER;
    }
    if (this.pos.y > (this.game.border.yMax + this.size.d)) {
      this.pos.y = FLAG_OVER;
    }
  }

  isCrash(target: Stuff | undefined) {
    if (!target) return false;
    const dx = Math.abs(target.pos.x - this.pos.x);
    const dy = Math.abs(target.pos.y - this.pos.y);
    const distance = dx * dx + dy * dy;
    const minrange = target.size.r + this.size.r;
    if ((minrange * minrange) < distance) return false;
    else return true;
  }
}

class Bullet extends Stuff {
  constructor(game:Game, x:number, y:number) {
    super(
      { r: 5, d: 10 },
      { angle: 0, x, y, },
      { angle: 0, x: 0, y: -10 },
      game,
    )
  }

  draw () {
    if (this.size.r === 0) return;
    this.game.context.strokeStyle = 'silver';
    this.game.context.lineWidth = 3;
    this.game.context.beginPath();
    this.game.context.arc(
      this.pos.x, this.pos.y, 2, 0,
      Math.PI * 2, true
    );
    this.game.context.stroke();
  }
}

class ImageStuff extends Stuff {
  protected img: any;

  rotate (angle: number) {
    const cvs = document.createElement("canvas");
    cvs.width = this.size.d;
    cvs.height = this.size.d;
    const ctx = cvs.getContext("2d");
    if (!!ctx) {
      ctx.save();
      ctx.translate(this.size.r, this.size.r);
      ctx.rotate(angle * Math.PI/180);
      ctx.translate(-this.size.r, -this.size.r);
      ctx.drawImage(this.img, 0, 0, this.size.d, this.size.d);
      ctx.restore();
    }
    return cvs;
  }

  draw() {
    if (this.size.r === 0) return;
    this.game?.context?.drawImage(
      (!this.pos.angle) ? this.img : this.rotate(this.pos.angle),
      this.pos.x - this.size.r, this.pos.y - this.size.r, 
      this.size.d, this.size.d
    );
  }
}

class Aircraft extends ImageStuff {
  constructor(game:Game) {
    const { xMax, yMax } = game.border;
    const r = 30;
    const d = r * 2;
    super(
      { r, d, },
      { angle: 0, x: xMax / 2, y: yMax - r },
      { angle: 0, x: 0, y: 0 },
      game,
    )
    this.img = new Image(d, d);
    this.img.src = `game/aircraft.svg`;
  }
}

class Rock extends ImageStuff {
  constructor(game:Game) {
    const r = 2 * Math.floor( Math.random() * 7 ) + 12;
    const d = 2 * r;
    super(
      { r, d },
      { 
        angle: Math.floor( Math.random() * 360 ),
        x: Math.floor( Math.random() * game.border.xMax ),
        y: r,
      },
      {
        angle: Math.floor( Math.random() * 7 ),
        x: Math.floor( Math.random() * 6 ) - 3,
        y: Math.floor( Math.random() * 3 ) + 1,
      },
      game,
    )
    this.img = new Image(d, d);
    this.img.src = `game/rock${Math.floor(Math.random() * 6) + 1}.svg`;
  }
}

export type TGameStatus = 'unknown' | 'ready' | 'playing' | 'paused' | 'game over';
export class Game {
  public readonly border: Border = { xMin: 0, yMin: 0, xMax: 450, yMax: 650 };
  readonly rockNumber = 7;
  readonly refreshRate= 30;
  private timer: any = null;

  private canvasRef: any; // !!! current !!!
  public context: any;

  constructor (ref:any, ctx:any) {
    if (!ref || !ctx) return;
    // else
    ref.width  = this.border.xMax;
    ref.height = this.border.yMax;
    this.canvasRef = ref;
    this.context = ctx;
  }

  public status:TGameStatus = 'unknown';
  private rocks: Rock[] = [];
  private bullets: Bullet[] = [];
  private aircraft?: Aircraft = undefined;

  clearAll () {
    this.canvasRef.width  = this.border.xMax;
    this.canvasRef.height = this.border.yMax;
  }
  drawAll () {
    this.rocks?.forEach((rock) => {
      rock.draw();
    })
    this.bullets?.forEach((bullet) => {
      bullet.draw();
    })
    this.aircraft?.draw();
  }

  generateRocks () {
    while(this.rocks.length < this.rockNumber) {
      this.rocks.push(new Rock(this));
    }
  }
  generateAircraft() {
    this.aircraft = new Aircraft(this);
  }
  generateStuff() {
    this.generateRocks();
    this.generateAircraft();
  }

  initial () {
    this.generateStuff();
    this.status = 'ready';
    this.drawAll();
    this.aircraft?.draw();
  }

  start () {
    if (!this.timer) {
      this.timer = setInterval(() => {
        this.moveAutoStuff();
      }, this.refreshRate);
      this.status = 'playing';
    }
  }
  pause () {
    if (!!this.timer) {
      clearInterval(this.timer);
      this.timer = null;
      this.status = 'paused';
    }
  }
  reset () {
    this.pause();
    this.clearAll();
    this.status = 'unknown';
    this.rocks = [];
    this.generateStuff();
    this.status = 'ready';
    this.drawAll();
  }
  moveAircraft(offset: number) {
    this.aircraft?.move(offset);
    this.clearAll();
    this.drawAll();
  }
  moveBullets () {
    this.bullets.forEach((bullet) => {
      bullet.move();
    })
  }
  moveRocks () {
    this.rocks.forEach((rock) => {
      rock.move();
    })
    for (let i=0;i<this.rockNumber;i++) {
      if (this.rocks[i].pos.y === FLAG_OVER) {
        delete this.rocks[i];
        this.rocks[i] = new Rock(this);
      }
    }
  }
  checkCrash () {
    for (let i=0;i<this.rockNumber;i++) {
      if ((this.rocks[i].size.r > 0) && this.rocks[i].isCrash(this.aircraft)) {
        return true;
      }
    }
    return false;
  }
  dealWithHitting () {
    const bl = this.bullets.length;
    for (let i=0;i<this.rockNumber;i++) {
      if (this.rocks[i].size.r === 0) continue;
      for (let j=0;j<bl;j++) {
        if (this.bullets[j].size.r === 0) continue;
        if (this.rocks[i].isCrash(this.bullets[j])) {
          this.rocks[i].size.r = 0; 
          this.rocks[i].size.d = 0;
          this.bullets[j].size.r = 0;
          this.bullets[j].size.d = 0;
        }
      }
    }
    this.bullets = this.bullets.filter((bullet) => (bullet.size.r > 0));
  }
  moveAutoStuff() {
    this.clearAll();

    this.moveBullets();
    this.moveRocks();
    if (this.checkCrash()) {
      this.pause();
      this.status = 'game over';
    }
    this.dealWithHitting();
    this.drawAll();
  }
  fire () {
    if (!!this.aircraft) {
      this.bullets.push(
        new Bullet(this, this.aircraft.pos.x, this.border.yMax - this.aircraft.size.d)
      );
    }
  }
}
